[TRTLLM-11958][perf] reduce @torch.library.custom_op host overhead - #13149
Merged
luyiyun1021 merged 3 commits intoApr 24, 2026
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@coderabbitai summary
Description
@torch.library.custom_opwraps every call in Python-level wrappers registered on several dispatch keys (Autograd, optionally ADInplaceOrView, and the backend key), which imposes a ~7us per-call dispatcher tax on top of the actual kernel launch. On host-heavy iterations (LTX-2 dense transformer issues ~2100 such calls per step for the two ops in this PR) the tax becomes a measurable fraction of the per-step wall time.This PR:
fast_custom_opthat registers an op directly through the low-leveltorch.library.Library.define + implAPI. The helper preserves the@custom_opdeveloper experience — schema is inferred from Python type hints viatorch.library.infer_schema, and the returned object exposes.register_fake— while bypassing the multi-layer Python wrappers that@custom_opinstalls.trtllm::nvfp4_gemmandtrtllm::tunable_fp4_quantize) to@fast_custom_op.Approach — usage
The helper uses
torch.library.infer_schemainternally so the schema is still driven by Python type hints — no hand-written schema strings.FRAGMENTmode is used under the hood because thetrtllmnamespace is already declared by C++ viaTORCH_LIBRARY_FRAGMENT.Why
@custom_opis expensive — code-level analysisLooking at
torch/_library/custom_ops.py::CustomOpDef._register_to_dispatcher(L607-675),@custom_opregisters several Python-level kernels on multiple dispatch keys. Each one is executed on every call:1. Autograd key (always registered) —
torch/_library/autograd.py::autograd_impl(L108):Every call pays:
is_grad_enabled()+_any_requires_grad(*args)tensor iteration +Metadatadataclass construction +_AutoDispatchBelowAutogradcontext manager +op.redispatch(...)(a second dispatch trip).2. ADInplaceOrView key —
adinplaceorview_impl(L654). Registered only when the schema is mutable or a view op. Bumps version counters on mutated args and routes throughcall_boxed. Not a cost for the two ops in this PR (both are pure,mutates_args=()), so schema is non-mutable and this wrapper is not installed.3. CUDA backend key —
backend_implwrapper fromregister_kernel(L346-362):Every call pays an aliasing-constraint check against
self._opoverload._schema(iterates inputs/outputs, compares storage pointers) and a closure construction.4.
CustomOpDef.__call__— L697. One extra Python frame when the op is invoked by the decorated name (e.g.nvfp4_gemm(x)in-module). Call sites that go throughtorch.ops.trtllm.nvfp4_gemm(x)bypass this frame.What the low-level
Library.define + implpath doesrequires_grad=Truefalls through to the dispatcher's C++-level "no autograd kernel" path (same user-facing error as before), no Python wrapper runs on the common inference path where no tensor needs grad.backend_implwrapper, no aliasing check on each call.Call path comparison (same
torch.ops.trtllm.my_op(x)invocation)@custom_oppath (pure, non-mutating op):Library.define + implpath:Two fewer Python frames + no aliasing check + no re-dispatch.
Microbenchmark
Isolated per-call cost on B200 + PyTorch 2.10, 20k-iter tight loop with
x.clone()as a minimal kernel (tmp/bench_custom_op_overhead_v2.py):@custom_op@torch.library.custom_op(baseline)Library.define + impl@fast_custom_op(viatorch.ops)@fast_custom_op(via proxy__call__)*Pure dispatcher tax = total per-call minus the plain-Python-fn kernel floor.
Key observations:
@fast_custom_op's hot path (torch.ops.trtllm.<name>(...)) is byte-identical to manualLibrary.define + implat runtime: both resolve to the sameOpOverloadand skip all the Python wrappers. No wrapper overhead from the helper itself.__call__path is marginally cheaper because theOpOverloadobject is cached at decoration time.Estimated contribution on LTX-2 at baseline (
@custom_op, 12us/call gross):trtllm::tunable_fp4_quantizetrtllm::nvfp4_gemmEnd-to-end measurements
Per-step host-time attribution, 1 GPU non-cuda-graph vs compile modes (from nsys,
denoise_stepNVTX range, baseline@custom_opstate):@custom_opcontribution (12us × 2100 ≈ 25.2 ms)The E2E win scales with how much other host overhead (kernel launch APIs) has already been absorbed by
torch.compile+ CUDA Graph — ontorch.compile + cuda_graphthe@custom_opwrapper becomes a large fraction of what's left.When to keep
@custom_opmutates_args=("out",)trtllm::bmm_out)setup_context+backwardtorch.compile— only fires whenmutates_argsis non-emptymutates_args=()makes this a no-opKeep
@torch.library.custom_opfor ops that (a) have non-emptymutates_args, (b) need autograd, or (c) are under active development and benefit from the richer Python-side error messages.Functional equivalence
After the switch,
torch.ops.trtllm.nvfp4_gemmandtorch.ops.trtllm.tunable_fp4_quantizeresolve to the sameOpOverloadobjects with the same schema string as before — so eager Python,torch.compile, Dynamo, FX passes, and any downstream FX pattern matcher (e.g.ar_residual_normthat looks fortorch.ops.trtllm.nvfp4_gemm.default) all see an identical op. The only difference is the dispatch path, which is shorter.register_fakestill provides the same FakeTensor/meta shape inference used bytorch.compiletracing.Side-effect validation
Verified parity with
@custom_opbaseline via 13 targeted tests: numerical correctness,torch.compile(fullgraph + dynamic), FakeTensor/meta propagation,inference_mode/no_grad, mutation-safety (args not mutated, output not aliased to inputs), error cases (wrong dtype/device) raising identicalRuntimeError, CPU-tensor dispatch failing identically, autograd-path withrequires_grad=Trueraising the same error (both ops are non-differentiable by design).E2E smoke test: 10-step LTX-2 1 GPU run with
torch_compile=trueusing the@fast_custom_opform — exit code 0, steady-state 0.75s/step matches the manual-Library form.Test Coverage
tests/unittest/_torch/thop/test_nvfp4_gemm.pycoversnvfp4_gemm— passes unchanged.tunable_fp4_quantizeis exercised by all NVFP4 Linear / MoE tests and the LTX-2 integration.PR Checklist
PR description clearly explains what and why.
PR Follows TRT-LLM CODING GUIDELINES.
Test cases are provided for new code paths.
Any new dependencies have been scanned for license and vulnerabilities.
CODEOWNERS updated if ownership changes.
Documentation updated as needed.
Update tava architecture diagram if significant design change.
Please check this after reviewing the above items as appropriate for this PR.
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.